﻿
namespace Patterns4Net.PatternEnforcer.Core.MethodCheckers
{
    using System.Diagnostics.Contracts;
    using System.Linq;
    using Mono.Cecil;
    using Mono.Cecil.Cil;
    using Patterns4Net.Core.CecilExtensions;
    using Patterns4Net.Core.CecilExtensions.CILPatternsMatching;
    using System.Collections.Generic;

    /// <summary>
    /// Checks whether the method contains minimal implementation which is empty body in case of void methods or 
    /// just return expression in case of non-void methods.
    /// </summary>
    public class HasMinimalImplementation : IChecker<MethodDefinition>
    {
        public CheckerResult Check(MethodDefinition method)
        {
            var error = new CheckerError(string.Format(ErrorMessages.MethodIsTooComplex, method.Name, method.DeclaringType.Name));
            return CheckerResult.CreateSimpleResult(IsValid(method), error);
        }

        private static bool IsValid(MethodDefinition method)
        {
            if (method.MethodReturnType.IsVoid())
            {
                var retMatcher = new CILPatternsMatcher(new OpCodeInstructionMatcher(OpCodes.Ret));
                return retMatcher.Matches(method.Body.Instructions.First());                    
            }

            // what does not have to be in method with just return statement? e.g. return new X(new Y(), 3);             
            var forbidden = new[]
                {
                    // conditional branching
                    OpCodes.Break,
                    OpCodes.Brfalse_S,
                    OpCodes.Brtrue,
                    OpCodes.Brtrue_S,

                    // parameters manipulation
                    OpCodes.Ldarg,
                    OpCodes.Ldarg_0,
                    OpCodes.Ldarg_1,
                    OpCodes.Ldarg_2,
                    OpCodes.Ldarg_3,
                    OpCodes.Ldarg_S,
                    OpCodes.Ldarga,
                    OpCodes.Ldarga_S,
                    OpCodes.Starg,
                    OpCodes.Starg_S,

                    // arithmetic or boolean expressions
                    OpCodes.Add,
                    OpCodes.Add_Ovf,
                    OpCodes.Add_Ovf_Un,
                    OpCodes.Sub,
                    OpCodes.Sub_Ovf,
                    OpCodes.Sub_Ovf_Un,
                    OpCodes.Mul,
                    OpCodes.Mul_Ovf,
                    OpCodes.Mul_Ovf_Un,
                    OpCodes.Div,
                    OpCodes.Div_Un,
                    OpCodes.Not,
                    OpCodes.And,
                    OpCodes.Or,
                    OpCodes.Neg,
                    OpCodes.Shl,
                    OpCodes.Shr,
                };            

            return ContainsForbiddenInstruction(RemoveContractPreconditions(method), forbidden) == false;
        }

        private static bool ContainsForbiddenInstruction(IEnumerable<Instruction> instructions, OpCode[] forbidden)
        {
            return instructions.Any(i => forbidden.Any(notAllowed => notAllowed == i.OpCode));
        }

        private static IEnumerable<Instruction> RemoveContractPreconditions(MethodDefinition method)
        {
            var result = new List<Instruction>(method.Body.Instructions);
            var lastRequiresCall = result.LastOrDefault(i => 
                i.IsCallInstruction() &&
                ((MethodReference)i.Operand).DeclaringType.FullName.StartsWith("System.Diagnostics.Contracts") &&
                ((MethodReference)i.Operand).Name == "Requires");
            if (lastRequiresCall != null)
            {
                result.RemoveRange(0, result.LastIndexOf(lastRequiresCall) + 1);
            }

            return result;
        }
    }
}
